package src;

import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.IOException;
import java.net.UnknownHostException;


/**
 * NetworkManager is in charge of listening for connections from and initating
 * connections to remote hosts. Because it must wait for new connections, the
 * class is designed as a thread, with a main loop that simply blocks while
 * waiting for new connections to arrive.
 * 
 * The class also contains methods to initiate outgoing connections in response
 * to commands from the main class.
 * 
 * Once a socket has been created (either receving or outgoing), this class
 * attaches two new threads to it: one for reading data from the socket, and one
 * for writing new data out to it. These threads are implemented using the
 * NetworkSender and NetworkReceiver classes.
 * 
 * @author Anurag, Mr. Peck
 * @version May 26, 2014
 * @author Period: 4
 * @author Assignment: SuperTicTacToe
 * 
 * @author Sources: NetTileGW
 */
public class NetworkManager extends Thread
{
    /** Object to send client notifications to */
    protected NetworkClient client;

    /** Server socket to listen on */
    protected ServerSocket serverSocket;

    /** Collection of sending threads */
    protected Map<SocketName, NetworkSender> senders;

    /** Collection of receiving threads */
    protected Map<SocketName, NetworkReceiver> receivers;

    /** Count of incoming connections (used for unique naming) */
    protected int count = 1;

    /**
     * Keeps track of who is the "master" - goes first.
     */
    protected boolean master = false;


    /**
     * Constructor. Creates a new thread listening on the given port. Also,
     * remembers the client object, which it passes on to other threads as they
     * are created.
     * 
     * @param c
     *            The client object to pass on to child threads
     * @param p
     *            The port to bind to and listen on
     */
    public NetworkManager( NetworkClient c, int p )
    {
        super( "NetworkManager-" + p );

        senders = new HashMap<SocketName, NetworkSender>();
        receivers = new HashMap<SocketName, NetworkReceiver>();

        client = c;

        try
        {
            serverSocket = new ServerSocket( p );
            start();
        }
        catch ( IOException e )
        {
            System.err.println( "Could not listen on port " + p );
        }
    }


    /**
     * Helper method for main spawn method. This version deduces a SocketName
     * object from data available from the socket, and can be used for
     * connections where the address is not known ahead of time (e.g., incoming
     * connections).
     * 
     * @param sock
     *            The Socket to spawn listeners for
     * 
     * @see spwan(SocketName, Socket)
     */
    protected synchronized void spawn( Socket sock )
    {
        SocketName name = new SocketName( sock.getInetAddress().toString(),
            sock.getPort(),
            "Incoming " + count );
        count++;
        spawn( name, sock );
    }


    /**
     * Helper method that creates two new threads to listen on the given socket.
     * Also, stores the threads in a data structure so they can be easily
     * referenced later (when they need to have messages sent to them, or when
     * they need to be killed off).
     * 
     * @param name
     *            The name of the socket
     * @param sock
     *            The Socket to spawn listeners for
     */
    protected void spawn( SocketName name, Socket sock )
    {
        NetworkSender s = new NetworkSender( client, name, sock );
        NetworkReceiver r = new NetworkReceiver( client, name, sock );

        senders.put( name, s );
        receivers.put( name, r );

        client.createSocket( name );
    }


    /**
     * Main thread method. Should simply contain a loop that waits for incoming
     * connections and spawn()s new threads for them as appropriate.
     */
    public void run()
    {

        // listen until we quit
        try
        {

            while ( true )
            {
                Socket s = serverSocket.accept();
                spawn( s );
            }

        }
        catch ( IOException ioe )
        {
            ioe.printStackTrace();
        }
    }


    /**
     * Create a new outgoing connection socket with the parameters provided in
     * the socket name.
     * 
     * Also, notify the client class of the new socket once it's created.
     * 
     * @param name
     *            The name and connection parameters of the socket to create
     */
    public void connect( SocketName name )
    {
        try
        {
            Socket sock = new Socket( name.getHost(), name.getPort() );
            spawn( name, sock );
        }
        catch ( UnknownHostException e )
        {
            master = true;
            System.err.println( "Unknown host: " + name.getHost() );

        }
        catch ( IOException e )
        {
            master = true; // if it's the first person, it goes to this catch
                           // which makes the variable go true!
            e.printStackTrace();
        }

    }


    /**
     * Destroy an existing connection socket with the parameters provided in the
     * socket name.
     * 
     * Also, notify the client class that the socket has been destroyed.
     * 
     * @param name
     *            The name and connection parameters of the socket to destroy
     */
    public void disconnect( SocketName name )
    {
        System.out.println( "Disconnecting from '" + name + "'" );

        NetworkSender s = senders.remove( name );
        NetworkReceiver r = receivers.remove( name );

        s.kill();
        r.kill();

        client.destroySocket( name );
    }


    /**
     * Iterate over all socket sending threads and tell them to send the given
     * data.
     * 
     * @param s
     *            The data to send
     */
    public void send( String s )
    {
        Iterator iter = senders.keySet().iterator();

        while ( iter.hasNext() )
        {
            NetworkSender cs = senders.get( iter.next() );
            cs.send( s );
        }
    }

}
